단어 간의 관계를 살펴보는 분석 방법
‘손-장갑’, ‘머리-모자’ 처럼 관계가 있는 단어 파악
단어의 관계를 표현한 의미망(semantic network) 만드는데 활용
# 기생충 기사 댓글 불러오기
library(readr)
raw_news_comment <- read_csv("news_comment_parasite.csv")
# 전처리
library(dplyr)
library(stringr)
library(textclean)
news_comment <- raw_news_comment %>%
select(reply) %>%
mutate(reply = str_replace_all(reply, "[^가-힣]", " "),
reply = str_squish(reply),
id = row_number())
SimplePos22() : 문장의 단어를 22개의 품사로 구분library(tidytext)
library(KoNLP)
comment_pos <- news_comment %>%
unnest_tokens(input = reply,
output = word,
token = SimplePos22,
drop = F)
comment_pos %>%
select(word, reply)
## # A tibble: 39,956 x 2
## word reply
## <chr> <chr>
## 1 정말/ma 정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 일인 ~
## 2 우리/np 정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 일인 ~
## 3 집/nc+에/jc 정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 일인 ~
## 4 좋/pa+은/et 정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 일인 ~
## 5 일/nc+이/jc 정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 일인 ~
## 6 생기/pv+어/ec 정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 일인 ~
## 7 기쁘/pa+고/ec 정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 일인 ~
## 8 행복한/nc 정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 일인 ~
## 9 것/nb+처럼/jc 정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 일인 ~
## 10 나/np+의/jc 정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 일인 ~
## # ... with 39,946 more rows
원하는 품사를 추출하기 쉽도록 한 행을 한 품사로 구성하기
tidyr::separate_rows():
정규 표현식에 따라 텍스트를 여러 행으로 나누기
sep = "[+]": "+"가 등장할 때마다 행을 나눔
# 품사별로 행 분리
library(tidyr)
comment_pos <- comment_pos %>%
separate_rows(word, sep = "[+]")
comment_pos %>%
select(word, reply)
## # A tibble: 66,898 x 2
## word reply
## <chr> <chr>
## 1 정말/ma 정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 일인 양 행복~
## 2 우리/np 정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 일인 양 행복~
## 3 집/nc 정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 일인 양 행복~
## 4 에/jc 정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 일인 양 행복~
## 5 좋/pa 정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 일인 양 행복~
## 6 은/et 정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 일인 양 행복~
## 7 일/nc 정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 일인 양 행복~
## 8 이/jc 정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 일인 양 행복~
## 9 생기/pv 정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 일인 양 행복~
## 10 어/ec 정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 일인 양 행복~
## # ... with 66,888 more rows
"/n"이 붙어있는 단어 추출# 명사 추출하기
noun <- comment_pos %>%
filter(str_detect(word, "/n")) %>%
mutate(word = str_remove(word, "/.*$"))
noun %>%
select(word, reply)
## # A tibble: 28,158 x 2
## word reply
## <chr> <chr>
## 1 우리 정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 일인 양 행복~
## 2 집 정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 일인 양 행복~
## 3 일 정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 일인 양 행복~
## 4 행복한 정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 일인 양 행복~
## 5 것 정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 일인 양 행복~
## 6 나 정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 일인 양 행복~
## 7 일 정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 일인 양 행복~
## 8 양 정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 일인 양 행복~
## 9 행복 정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 일인 양 행복~
## 10 행복 정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 일인 양 행복~
## # ... with 28,148 more rows
# 명사 빈도 구하기
noun %>%
count(word, sort = T)
## # A tibble: 8,866 x 2
## word n
## <chr> <int>
## 1 영화 460
## 2 기생충 443
## 3 봉준호 339
## 4 것 328
## 5 축하 260
## 6 아카데미 252
## 7 대단 241
## 8 나 225
## 9 대한민국 225
## 10 자랑 217
## # ... with 8,856 more rows
동사 "/pv", 형용사:"/pa" 붙어있는 단어 추출
단어 뒤에 태그 대신 ’다’를 붙여 이해하기 편하게 수정하기
# 동사, 형용사 추출하기
pvpa <- comment_pos %>%
filter(str_detect(word, "/pv|/pa")) %>% # "/pv", "/pa" 추출
mutate(word = str_replace(word, "/.*$", "다")) # "/"로 시작 문자를 "다"로 바꾸기
pvpa %>%
select(word, reply)
## # A tibble: 4,977 x 2
## word
## <chr>
## 1 좋다
## 2 생기다
## 3 기쁘다
## 4 축하드리다
## 5 기쁘다
## 6 기쁘다
## 7 기쁘다
## 8 축하드리다
## 9 불다
## 10 크다
## reply
## <chr>
## 1 정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 ~
## 2 정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 ~
## 3 정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 ~
## 4 정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 ~
## 5 정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 ~
## 6 와 너무 기쁘다 이 시국에 정말 내 일같이 기쁘고 감사하다 ~
## 7 와 너무 기쁘다 이 시국에 정말 내 일같이 기쁘고 감사하다 ~
## 8 와 너무 기쁘다 이 시국에 정말 내 일같이 기쁘고 감사하다 ~
## 9 우리나라의 영화감독분들 그리고 앞으로 그 꿈을 그리는 분~
## 10 우리나라의 영화감독분들 그리고 앞으로 그 꿈을 그리는 분~
## # ... with 4,967 more rows
추출한 단어 결합하기
이해할 수 있는 두 글자 이상 단어만 남기기
# 품사 결합
comment <- bind_rows(noun, pvpa) %>%
filter(str_count(word) >= 2) %>%
arrange(id)
comment %>%
select(word, reply)
## # A tibble: 27,535 x 2
## word reply
## <chr> <chr>
## 1 우리 정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 일인 양 ~
## 2 행복한 정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 일인 양 ~
## 3 행복 정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 일인 양 ~
## 4 행복 정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 일인 양 ~
## 5 좋다 정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 일인 양 ~
## 6 생기다 정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 일인 양 ~
## 7 기쁘다 정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 일인 양 ~
## 8 축하드리다 정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 일인 양 ~
## 9 기쁘다 정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 일인 양 ~
## 10 시국 와 너무 기쁘다 이 시국에 정말 내 일같이 기쁘고 감사하다 축하드려~
## # ... with 27,525 more rows
comment_new <- comment_pos %>%
separate_rows(word, sep = "[+]") %>%
filter(str_detect(word, "/n|/pv|/pa")) %>%
mutate(word = ifelse(str_detect(word, "/pv|/pa"),
str_replace(word, "/.*$", "다"),
str_remove(word, "/.*$"))) %>%
filter(str_count(word) >= 2) %>%
arrange(id)
]
widyr::pairwise_count()
item: 단어feature: 텍스트 구분 기준sort = T: 빈도가 높은 순으로 출력 결과 정렬install.packages("widyr")
library(widyr)
pair <- comment %>%
pairwise_count(item = word,
feature = id,
sort = T)
pair
## # A tibble: 263,462 x 3
## item1 item2 n
## <chr> <chr> <dbl>
## 1 영화 기생충 111
## 2 기생충 영화 111
## 3 감독 봉준호 86
## 4 봉준호 감독 86
## 5 감독님 봉준호 66
## 6 봉준호 감독님 66
## 7 만들다 영화 55
## 8 영화 만들다 55
## 9 블랙리스트 감독 54
## 10 감독 블랙리스트 54
## # ... with 263,452 more rows
pair %>% filter(item1 == "영화")
## # A tibble: 2,429 x 3
## item1 item2 n
## <chr> <chr> <dbl>
## 1 영화 기생충 111
## 2 영화 만들다 55
## 3 영화 봉준호 47
## 4 영화 받다 45
## 5 영화 아카데미 42
## 6 영화 같다 41
## 7 영화 감독 39
## 8 영화 아니다 38
## 9 영화 한국 34
## 10 영화 좋다 33
## # ... with 2,419 more rows
pair %>% filter(item1 == "봉준호")
## # A tibble: 1,491 x 3
## item1 item2 n
## <chr> <chr> <dbl>
## 1 봉준호 감독 86
## 2 봉준호 감독님 66
## 3 봉준호 기생충 49
## 4 봉준호 영화 47
## 5 봉준호 블랙리스트 43
## 6 봉준호 대한민국 37
## 7 봉준호 자랑 29
## 8 봉준호 대단 28
## 9 봉준호 축하 27
## 10 봉준호 아카데미 24
## # ... with 1,481 more rows
동시 출현 빈도를 이용해 단어의 관계를 네트워크 형태로 표현
단어들이 어떤 맥락에서 함께 사용되었는지 이해할 수 있다
tidygraph::as_tbl_graph()네트워크가 너무 복잡하지 않도록 25회 이상 사용된 단어 추출해 생성
install.packages("tidygraph")
library(tidygraph)
graph_comment <- pair %>%
filter(n >= 25) %>%
as_tbl_graph()
graph_comment
## # A tbl_graph: 26 nodes and 88 edges
## #
## # A directed simple graph with 2 components
## #
## # Node Data: 26 x 1 (active)
## name
## <chr>
## 1 영화
## 2 기생충
## 3 감독
## 4 봉준호
## 5 감독님
## 6 만들다
## # ... with 20 more rows
## #
## # Edge Data: 88 x 3
## from to n
## <int> <int> <dbl>
## 1 1 2 111
## 2 2 1 111
## 3 3 4 86
## # ... with 85 more rows
ggraph::ggraph()install.packages("ggraph")
library(ggraph)
ggraph(graph_comment) +
geom_edge_link() + # 엣지
geom_node_point() + # 노드
geom_node_text(aes(label = name)) # 텍스트
이미지 출력 창을 별도로 열어 큰 화면에서 보기 - 윈도우: windows() - macOS: x11()
# 한글 폰트 설정
library(showtext)
font_add_google(name = "Nanum Gothic", family = "nanumgothic")
showtext_auto()
ggraph(layout = "fr"): 네트워크 형태 결정
set.seed()로 난수 고정set.seed(1234) # 난수 고정
ggraph(graph_comment, layout = "fr") + # 레이아웃
geom_edge_link(color = "gray50", # 엣지 색깔
alpha = 0.5) + # 엣지 명암
geom_node_point(color = "lightcoral", # 노드 색깔
size = 5) + # 노드 크기
geom_node_text(aes(label = name), # 텍스트 표시
repel = T, # 노드밖 표시
size = 5, # 텍스트 크기
family = "nanumgothic") + # 폰트
theme_graph() # 배경 삭제
노드 텍스트 폰트 geom_node_text()의 family로 별도 설정. theme()으로 적용 안됨.
word_network <- function(x) {
ggraph(x, layout = "fr") +
geom_edge_link(color = "gray50",
alpha = 0.5) +
geom_node_point(color = "lightcoral",
size = 5) +
geom_node_text(aes(label = name),
repel = T,
size = 5,
family = "nanumgothic") +
theme_graph()
}
set.seed(1234)
word_network(graph_comment)
유의어(synonyms): 표현은 다르지만 의미가 비슷한 단어
"감독", "봉감독", "봉준호감독"유의어 통일하기: 네트워크 구조가 간결해지고 단어의 관계가 좀 더 분명하게 드러남
library(ggraph)
# 유의어 처리하기
comment <- comment %>%
mutate(word = ifelse(str_detect(word, "감독") &
!str_detect(word, "감독상"), "봉준호", word),
word = ifelse(word == "오르다", "올리다", word),
word = ifelse(str_detect(word, "축하"), "축하", word))
# 단어 동시 출현 빈도 구하기
pair <- comment %>%
pairwise_count(item = word,
feature = id,
sort = T)
# 네트워크 그래프 데이터 만들기
graph_comment <- pair %>%
filter(n >= 25) %>%
as_tbl_graph()
# 네트워크 그래프 만들기
set.seed(1234)
word_network(graph_comment)
네트워크 그래프는 단어 노드가 많아 어떤 단어 노드 중심으로 해석할지 판단 어려움
연결 중심성과 커뮤니티를 표현하면 단어의 관계를 더 분명하게 파악할 수 있다
노드가 다른 노드들과 얼마나 밀접하게 연결되는지 나타낸 값
연결 중심성으로 노드 크기를 조정하면 어떤 단어를 눈여겨봐야 할지 판단하기 쉬워진다
네트워크 그래프는 단어 노드가 많아 어떤 단어 노드 중심으로 해석할지 판단 어려움
연결 중심성과 커뮤니티를 표현하면 단어의 관계를 더 분명하게 파악할 수 있다
단어 간의 관계가 가까워 빈번하게 연결된 노드 집단
노드를 커뮤니티별로 구분 지어 서로 다른 색으로 표현하면 네트워크 구조를 이해하기 쉬워진다
as_tbl_graph()
directed = F: 방향성 없도록 설정group_infomap()은 방향성 없는 네트워크 그래프 데이터에서만 커뮤니티를 찾아줌centrality_degree()group_infomap()
as.factor(): factor 타입으로 변환해 노드 그룹별로 다른 색으로 표현set.seed(1234)
graph_comment <- pair %>%
filter(n >= 25) %>%
as_tbl_graph(directed = F) %>%
mutate(centrality = centrality_degree(), # 연결 중심성
group = as.factor(group_infomap())) # 커뮤니티
graph_comment
## # A tbl_graph: 33 nodes and 144 edges
## #
## # An undirected multigraph with 1 component
## #
## # Node Data: 33 x 3 (active)
## name centrality group
## <chr> <dbl> <fct>
## 1 봉준호 58 1
## 2 축하 32 2
## 3 영화 26 4
## 4 기생충 24 3
## 5 블랙리스트 6 5
## 6 자랑 6 2
## # ... with 27 more rows
## #
## # Edge Data: 144 x 3
## from to n
## <int> <int> <dbl>
## 1 1 2 197
## 2 1 2 197
## 3 1 3 113
## # ... with 141 more rows
geom_node_point(aes())
size = centrality: 연결 중심성에 따라 노드 크기 설정color = group: 커뮤니티 별로 노드 색깔 다르게geom_node_point(show.legend = F): 범례 제거scale_size(range = c(5, 15)): 노드 크기 5~15 범위 유지set.seed(1234)
ggraph(graph_comment, layout = "fr") + # 레이아웃
geom_edge_link(color = "gray50", # 엣지 색깔
alpha = 0.5) + # 엣지 명암
geom_node_point(aes(size = centrality, # 노드 크기
color = group), # 노드 색깔
show.legend = F) + # 범례 삭제
scale_size(range = c(5, 15)) + # 노드 크기 범위
geom_node_text(aes(label = name), # 텍스트 표시
repel = T, # 노드밖 표시
size = 5, # 텍스트 크기
family = "nanumgothic") + # 폰트
theme_graph() # 배경 삭제
graph_comment %>%
filter(name == "봉준호")
## # A tbl_graph: 1 nodes and 0 edges
## #
## # An unrooted tree
## #
## # Node Data: 1 x 3 (active)
## name centrality group
## <chr> <dbl> <fct>
## 1 봉준호 58 1
## #
## # Edge Data: 0 x 3
## # ... with 3 variables: from <int>, to <int>, n <dbl>
graph_comment %>%
filter(group == 4) %>%
arrange(-centrality) %>%
data.frame()
## name centrality group
## 1 영화 26 4
## 2 아카데미 10 4
## 3 만들다 4 4
graph_comment %>%
arrange(-centrality)
## # A tbl_graph: 33 nodes and 144 edges
## #
## # An undirected multigraph with 1 component
## #
## # Node Data: 33 x 3 (active)
## name centrality group
## <chr> <dbl> <fct>
## 1 봉준호 58 1
## 2 축하 32 2
## 3 영화 26 4
## 4 기생충 24 3
## 5 작품상 14 6
## 6 대한민국 10 1
## # ... with 27 more rows
## #
## # Edge Data: 144 x 3
## from to n
## <int> <int> <dbl>
## 1 1 2 197
## 2 1 2 197
## 3 1 3 113
## # ... with 141 more rows
graph_comment %>%
filter(group == 2) %>%
arrange(-centrality) %>%
data.frame()
## name centrality group
## 1 축하 32 2
## 2 자랑 6 2
## 3 같다 6 2
## 4 아니다 6 2
## 5 수상 4 2
## 6 멋지다 4 2
## 7 한국 2 2
## 8 기쁘다 2 2
news_comment %>%
filter(str_detect(reply, "봉준호") & str_detect(reply, "대박")) %>%
select(reply)
## # A tibble: 19 x 1
## reply
## <chr>
## 1 대박 대박 진짜 대박 봉준호 감독님과 우리 배우~
## 2 내가 죽기전에 아카데미에서 한국어를 들을줄이야~
## 3 대박 관왕이라니 축하합니다 봉준호를 배출한 충~
## 4 우와 대박 진자 대단하다 봉준호
## 5 봉준호 경사났네 대박중에 대에박 축하합니다
## 6 봉준호 작품상 탔다 대박
## 7 봉준호 군대 면제시켜도될듯 대박 여윽시 위대한 ~
## 8 아니 다른상을 받은것도 충분히 대단하고 굉장하~
## 9 봉준호 군대 면제시켜도될듯 대박 여윽시 위대한 ~
## 10 봉준호감독님대박 축하합니다
## # ... with 9 more rows
대부분의 단어와 자주 함께 사용되는 단어쌍 다수
"영화"-"기생충"다른 단어에 비해 상대적으로 자주 함께 사용된 단어가 무엇인지 살펴봐야 한다
두 단어가 함께 사용되는 경우가 각각 사용되는 경우에 비해 얼마나 많은지 나타낸 지표
상대적으로 관련성이 큰 단어 파악하는데 활용
어떤 단어와 자주 함께 사용되지만 다른 단어와는 자주 함께 사용되지 않는 단어
X, Y 두 단어가 있을 때, 여러 텍스트에서 두 단어의 사용 여부를 놓고 가능한 모든 경우
X, Y 모두 있음( \(a\) )
X, Y 모두 없음( \(d\) )
X만 있음( \(b\) )
Y만 있음( \(c\) )
\[\phi=\frac{ad-bc}{\sqrt{(a+b)(c+d)(a+c)(b+d)}}\]
-1 ~ +1
+1에 가까울수록 두 단어가 자주 함께 사용되어 관련성이 크다는 의미
-1에 가까울수록 함께 사용되는 경우가 드물어 관련성이 작다는 의미
widyr::pairwise_cor()
item: 단어feature: 텍스트 구분 기준sort = T: 파이 계수 높은순 정렬word_cors <- comment %>%
add_count(word) %>%
filter(n >= 20) %>%
pairwise_cor(item = word,
feature = id,
sort = T)
word_cors
원자료에 빈도 나타낸 변수 추가
## # A tibble: 25,440 x 3
## item1 item2 correlation
## <chr> <chr> <dbl>
## 1 올리다 블랙리스트 0.476
## 2 블랙리스트 올리다 0.476
## 3 역사 쓰다 0.345
## 4 쓰다 역사 0.345
## 5 박근혜 블랙리스트 0.314
## 6 블랙리스트 박근혜 0.314
## 7 정경심 조국 0.305
## 8 조국 정경심 0.305
## 9 가족 조국 0.302
## 10 조국 가족 0.302
## # ... with 25,430 more rows
word_cors %>%
filter(item1 == "대한민국")
## # A tibble: 159 x 3
## item1 item2 correlation
## <chr> <chr> <dbl>
## 1 대한민국 국민 0.189
## 2 대한민국 자랑 0.156
## 3 대한민국 위상 0.149
## 4 대한민국 위대한 0.100
## 5 대한민국 세계 0.0911
## 6 대한민국 문화 0.0758
## 7 대한민국 나라 0.0740
## 8 대한민국 감사합니 0.0725
## 9 대한민국 오늘 0.0716
## 10 대한민국 영화계 0.0685
## # ... with 149 more rows
word_cors %>%
filter(item1 == "역사")
## # A tibble: 159 x 3
## item1 item2 correlation
## <chr> <chr> <dbl>
## 1 역사 쓰다 0.345
## 2 역사 최초 0.121
## 3 역사 한국 0.113
## 4 역사 영광 0.0724
## 5 역사 아니다 0.0721
## 6 역사 한국영화 0.0719
## 7 역사 이것 0.0617
## 8 역사 영화제 0.0617
## 9 역사 소름 0.0594
## 10 역사 감사 0.0589
## # ... with 149 more rows
# 관심 단어 목록 생성
target <- c("대한민국", "역사", "수상소감", "봉준호")
top_cors <- word_cors %>%
filter(item1 %in% target) %>%
group_by(item1) %>%
slice_max(correlation, n = 8)
# 그래프 순서 정하기
top_cors$item1 <- factor(top_cors$item1, levels = target)
library(ggplot2)
ggplot(top_cors, aes(x = reorder_within(item2, correlation, item1),
y = correlation,
fill = item1)) +
geom_col(show.legend = F) +
facet_wrap(~ item1, scales = "free") +
coord_flip() +
scale_x_reordered() +
labs(x = NULL) +
theme(text = element_text(family = "nanumgothic"))
set.seed(1234)
graph_cors <- word_cors %>%
filter(correlation >= 0.15) %>%
as_tbl_graph(directed = F) %>%
mutate(centrality = centrality_degree(),
group = as.factor(group_components()))
graph_cors %>%
activate(nodes) %>%
as_tibble() %>%
arrange(group,-centrality)
## # A tibble: 58 x 3
## name centrality group
## <chr> <dbl> <fct>
## 1 블랙리스트 12 1
## 2 박근혜 8 1
## 3 송강호 8 1
## 4 올리다 4 1
## 5 이미경 4 1
## 6 봉준호 2 1
## 7 자한당 2 1
## 8 정권 2 1
## 9 배우 2 1
## 10 높다 6 2
## # ... with 48 more rows
set.seed(1234)
ggraph(graph_cors, layout = "fr") +
geom_edge_link(color = "gray50",
aes(edge_alpha = correlation, # 엣지 명암
edge_width = correlation), # 엣지 두께
show.legend = F) + # 범례 삭제
scale_edge_width(range = c(1, 4)) + # 엣지 두께 범위
geom_node_point(aes(size = centrality,
color = group),
show.legend = F) +
scale_size(range = c(5, 10)) +
geom_node_text(aes(label = name),
repel = T,
size = 5,
family = "nanumgothic") +
theme_graph()
동시 출현 빈도를 이용한 네트워크 그래프
여러 단어와 자주 함께 사용된 단어쌍 중심으로 네트워크 형성
노드 대부분이 서로 연결되어 구조가 복잡하고 군집이 잘 드러나지 않음
자주 사용된 단어를 파악할 때 활용
파이 계수를 이용한 네트워크 그래프
다른 단어에 비해 상대적으로 자주 함께 사용된 단어쌍 중심으로 네트워크 형성
관련성이 큰 단어끼리만 연결되어 단어 군집이 명확하게 드러남
밀접하게 관련된 단어쌍 파악할 때 활용
같은 단어도 함께 사용된 단어에 따라 의미가 달라짐
어떤 단어는 다른 단어와 연결되어 새로운 의미를 만들어냄
동시 출현 빈도와 파이 계수의 한계: 단어가 함께 사용된 횟수만 고려
단어가 연결될 때 생기는 의미 무시
이해하기 어려운 단어쌍 등장
단어가 연결될 때 생기는 의미를 고려하려면 ‘자주 연이어 사용된 단어’를 살펴봐야 한다
연이어 사용된 n개의 단어
두 단어 연속: 바이그램(bigram) 또는 2-gram
세 단어 연속: 트라이그램(trigram) 또는 3-gram
.center[ ]
텍스트를 엔그램으로 토큰화하면
단어 앞뒤에 연이어 사용된 단어를 함께 살펴봄: 얼마나 자주 ‘연이어’ 사용된 단어쌍인가?
단어가 연결될 때 생기는 의미와 맥락을 이해할 수 있음
대다수의 텍스트에 사용된 평범한 단어쌍이 아니라 분명한 의미를 드러내는 단어쌍 발견
샘플 텍스트로 엔그램 토큰화해보기
tidytext::unnest_tokens()
token = "ngrams"n: 기준 단어 수text <- tibble(value = "대한민국은 민주공화국이다. 대한민국의 주권은 국민에게 있고, 모든 권력은 국민으로부터 나온다.")
text
## # A tibble: 1 x 1
## value
## <chr>
## 1 대한민국은 민주공화국이다. 대한민국의 주권은 국민에게 있고, 모든 권력은 국민으로부터 나온다.
# 바이그램 토큰화
text %>%
unnest_tokens(input = value,
output = word,
token = "ngrams",
n = 2) #<<
## # A tibble: 9 x 1
## word
## <chr>
## 1 대한민국은 민주공화국이다
## 2 민주공화국이다 대한민국의
## 3 대한민국의 주권은
## 4 주권은 국민에게
## 5 국민에게 있고
## 6 있고 모든
## 7 모든 권력은
## 8 권력은 국민으로부터
## 9 국민으로부터 나온다
# 트라이그램 토큰화
text %>%
unnest_tokens(input = value,
output = word,
token = "ngrams",
n = 3) #<<
## # A tibble: 8 x 1
## word
## <chr>
## 1 대한민국은 민주공화국이다 대한민국의
## 2 민주공화국이다 대한민국의 주권은
## 3 대한민국의 주권은 국민에게
## 4 주권은 국민에게 있고
## 5 국민에게 있고 모든
## 6 있고 모든 권력은
## 7 모든 권력은 국민으로부터
## 8 권력은 국민으로부터 나온다
# 단어 기준 토큰화
text %>%
unnest_tokens(input = value,
output = word,
token = "words") #<<
## # A tibble: 10 x 1
## word
## <chr>
## 1 대한민국은
## 2 민주공화국이다
## 3 대한민국의
## 4 주권은
## 5 국민에게
## 6 있고
## 7 모든
## 8 권력은
## 9 국민으로부터
## 10 나온다
# 유니그램 토큰화
text %>%
unnest_tokens(input = value,
output = word,
token = "ngrams", #<<
n = 1) #<<
## # A tibble: 10 x 1
## word
## <chr>
## 1 대한민국은
## 2 민주공화국이다
## 3 대한민국의
## 4 주권은
## 5 국민에게
## 6 있고
## 7 모든
## 8 권력은
## 9 국민으로부터
## 10 나온다
comment_pos 이용: 댓글을 형태소로 토큰화 후 품사별로 행 분리
명사, 동사, 형용사를 추출해 결합한 후 두 글자 이상만 남김
comment_new <- comment_pos %>%
separate_rows(word, sep = "[+]") %>%
filter(str_detect(word, "/n|/pv|/pa")) %>%
mutate(word = ifelse(str_detect(word, "/pv|/pa"),
str_replace(word, "/.*$", "다"),
str_remove(word, "/.*$"))) %>%
filter(str_count(word) >= 2) %>%
arrange(id)
바이그램으로 토큰화할 때는 형태소 추출 먼저
텍스트 원문을 바이그램으로 토큰화하면 원형은 같지만 표현만 다른 단어들이 개별 단어로 취급됨
표현이 아니라 의미 중심으로 분석해야 하므로 형태소를 먼저 추출한 다음 바이그램으로 토큰화해야 함
comment_new <- comment_new %>%
mutate(word = ifelse(str_detect(word, "감독") &
!str_detect(word, "감독상"), "봉준호", word),
word = ifelse(word == "오르다", "올리다", word),
word = ifelse(str_detect(word, "축하"), "축하", word))
comment_new %>%
select(word)
## # A tibble: 27,535 x 1
## word
## <chr>
## 1 우리
## 2 좋다
## 3 생기다
## 4 기쁘다
## 5 행복한
## 6 행복
## 7 축하
## 8 행복
## 9 기쁘다
## 10 기쁘다
## # ... with 27,525 more rows
line_comment <- comment_new %>%
group_by(id) %>%
summarise(sentence = paste(word, collapse = " "))
line_comment
## # A tibble: 4,024 x 2
## id sentence
## <int> <chr>
## 1 1 우리 좋다 생기다 기쁘다 행복한 행복 축하 행복 기쁘다
## 2 2 기쁘다 시국 기쁘다 감사 축하 진심으
## 3 3 우리나라 봉준호 불다 크다 영감 봉준호 공동각본쓴 한진원 작가님 축하 축~
## 4 4 봉준호 봉준호 우리나라 대한민국 자랑 세계 어디 우리 한국인 힘내다 삽시
## 5 5 노벨상 탄느낌이네요 축하 합니
## 6 6 기생충 받다 박수 치다 감독상 기대다 봉준호 봉준호
## 7 7 대한민국 영화사 쓰다 계시다
## 8 8 저런게 아카데미상 받다 태극기 휘날리다 광해 명량 전부문 휩쓸어야겠
## 9 9 다시한번 보이다 영화관
## 10 10 대한민국 봉준호 대단 한국의 문화 자긍심 가지게합니
## # ... with 4,014 more rows
bigram_comment <- line_comment %>%
unnest_tokens(input = sentence,
output = bigram,
token = "ngrams",
n = 2)
bigram_comment
## # A tibble: 23,964 x 2
## id bigram
## <int> <chr>
## 1 1 우리 좋다
## 2 1 좋다 생기다
## 3 1 생기다 기쁘다
## 4 1 기쁘다 행복한
## 5 1 행복한 행복
## 6 1 행복 축하
## 7 1 축하 행복
## 8 1 행복 기쁘다
## 9 2 기쁘다 시국
## 10 2 시국 기쁘다
## # ... with 23,954 more rows
# 바이그램 분리하기
bigram_seprated <- bigram_comment %>%
separate(bigram, c("word1", "word2"), sep = " ")
bigram_seprated
## # A tibble: 23,964 x 3
## id word1 word2
## <int> <chr> <chr>
## 1 1 우리 좋다
## 2 1 좋다 생기다
## 3 1 생기다 기쁘다
## 4 1 기쁘다 행복한
## 5 1 행복한 행복
## 6 1 행복 축하
## 7 1 축하 행복
## 8 1 행복 기쁘다
## 9 2 기쁘다 시국
## 10 2 시국 기쁘다
## # ... with 23,954 more rows
# 단어쌍 빈도 구하기
pair_bigram <- bigram_seprated %>%
count(word1, word2, sort = T) %>%
na.omit()
pair_bigram
## # A tibble: 19,782 x 3
## word1 word2 n
## <chr> <chr> <int>
## 1 봉준호 봉준호 154
## 2 진심 축하 64
## 3 블랙리스트 올리다 62
## 4 봉준호 축하 57
## 5 대단 축하 38
## 6 영화 만들다 31
## 7 축하 봉준호 29
## 8 대박 축하 25
## 9 봉준호 블랙리스트 25
## 10 한국 영화 24
## # ... with 19,772 more rows
na.omit(): 결측치 행 제거: 한 단어로 된 문장은 바이그램으로 토큰화하면 NA가 됨
ex) ‘축하합니다’, ‘멋집니다’
# 동시 출현 단어쌍
pair %>% #<<
filter(item1 == "대한민국")
## # A tibble: 1,072 x 3
## item1 item2 n
## <chr> <chr> <dbl>
## 1 대한민국 봉준호 68
## 2 대한민국 축하 54
## 3 대한민국 자랑 43
## 4 대한민국 영화 29
## 5 대한민국 기생충 27
## 6 대한민국 국민 22
## 7 대한민국 세계 16
## 8 대한민국 대단 16
## 9 대한민국 아카데미 16
## 10 대한민국 위상 15
## # ... with 1,062 more rows
# 바이그램 단어쌍
pair_bigram %>% #<<
filter(word1 == "대한민국")
## # A tibble: 116 x 3
## word1 word2 n
## <chr> <chr> <int>
## 1 대한민국 국민 21
## 2 대한민국 자랑 13
## 3 대한민국 영화 10
## 4 대한민국 위상 7
## 5 대한민국 만세 6
## 6 대한민국 봉준호 5
## 7 대한민국 국격을 4
## 8 대한민국 문화 4
## 9 대한민국 기생충 3
## 10 대한민국 사람 3
## # ... with 106 more rows
# 네트워크 그래프 데이터 만들기
graph_bigram <- pair_bigram %>%
filter(n >= 8) %>%
as_tbl_graph()
# 네트워크 그래프 만들기
set.seed(1234)
word_network(graph_bigram)
bigram_seprated의 유의어 통일, 같은 단어 연속 단어쌍 제거
단어쌍 빈도 구하고 결측치 제거
# 유의어 처리
bigram_seprated <- bigram_seprated %>%
mutate(word1 = ifelse(str_detect(word1, "대단"), "대단", word1),
word2 = ifelse(str_detect(word2, "대단"), "대단", word2),
word1 = ifelse(str_detect(word1, "자랑"), "자랑", word1),
word2 = ifelse(str_detect(word2, "자랑"), "자랑", word2),
word1 = ifelse(str_detect(word1, "짝짝짝"), "짝짝짝", word1),
word2 = ifelse(str_detect(word2, "짝짝짝"), "짝짝짝", word2)) %>%
# 같은 단어 연속 제거
filter(word1 != word2)
# 단어쌍 빈도 구하기
pair_bigram <- bigram_seprated %>%
count(word1, word2, sort = T) %>%
na.omit()
# 네트워크 그래프 데이터 만들기
set.seed(1234)
graph_bigram <- pair_bigram %>%
filter(n >= 8) %>%
as_tbl_graph(directed = F) %>%
mutate(centrality = centrality_degree(), # 중심성
group = as.factor(group_infomap())) # 커뮤니티
# 네트워크 그래프 만들기
set.seed(1234)
ggraph(graph_bigram, layout = "fr") + # 레이아웃
geom_edge_link(color = "gray50", # 엣지 색깔
alpha = 0.5) + # 엣지 명암
geom_node_point(aes(size = centrality, # 노드 크기
color = group), # 노드 색깔
show.legend = F) + # 범례 삭제
scale_size(range = c(4, 8)) + # 노드 크기 범위
geom_node_text(aes(label = name), # 텍스트 표시
repel = T, # 노드밖 표시
size = 5, # 텍스트 크기
family = "nanumgothic") + # 폰트
theme_graph() # 배경 삭제
자주 연이어 사용된 단어쌍 중심으로 네트워크 형성
단어의 맥락과 의미를 구체적으로 이해할 수 있음
개별 단어의 빈도는 낮지만 자주 연이어 사용되고 함께 사용할 때 분명한 의미 지니는 단어쌍 발견
파이 계수를 이용한 네트워크 그래프
관련성이 큰 단어쌍 중심으로 네트워크 형성
빈도가 낮아도 관련성이 큰 단어 주로 표현
관련성이 작은 노드들이 연결되지 않음
바이그램을 이용한 네트워크 그래프
연이어 자주 사용된 단어쌍 중심으로 표현
관련성이 큰 동시에 자주 사용된 단어 주로 표현
노드가 대부분 연결됨
각 방법의 특징 다르므로 분석 목적에 맞게 선택
세 가지 방법 모두 사용해 분석 결과 비교하면 텍스트를 다각도로 이해할 수 있음
# 품사 기준 토큰화
comment_pos <- news_comment %>%
unnest_tokens(input = reply,
output = word,
token = SimplePos22,
drop = F)
# 명사, 동사, 형용사 추출
comment <- comment_pos %>%
separate_rows(word, sep = "[+]") %>%
filter(str_detect(word, "/n|/pv|/pa")) %>%
mutate(word = ifelse(str_detect(word, "/pv|/pa"),
str_replace(word, "/.*$", "다"),
str_remove(word, "/.*$"))) %>%
filter(str_count(word) >= 2) %>%
arrange(id)
# 단어 동시 출현 빈도 구하기
pair <- comment %>%
pairwise_count(item = word,
feature = id,
sort = T)
# 파이 계수 구하기
word_cors <- comment %>%
add_count(word) %>%
filter(n >= 20) %>%
pairwise_cor(item = word,
feature = id,
sort = T)
# 텍스트를 한 행으로 구성
line_comment <- comment %>%
group_by(id) %>%
summarise(sentence = paste(word, collapse = " "))
# 바이그램 토큰화
bigram_comment <- line_comment %>%
unnest_tokens(input = sentence,
output = bigram,
token = "ngrams",
n = 2)
# 바이그램 분리
bigram_seprated <- bigram_comment %>%
separate(bigram, c("word1", "word2"), sep = " ")
# 단어쌍 빈도 구하기
pair_bigram <- bigram_seprated %>%
count(word1, word2, sort = T) %>%
na.omit()
# 네트워크 그래프 데이터 만들기
set.seed(1234)
graph_comment <- pair_bigram %>%
filter(n >= 8) %>%
as_tbl_graph(directed = F) %>%
mutate(centrality = centrality_degree(),
group = as.factor(group_infomap()))
# 네트워크 그래프 만들기
set.seed(1234)
ggraph(graph_comment) +
geom_edge_link() +
geom_node_point(aes(size = centrality,
color = group)) +
geom_node_text(aes(label = name))